/**
 * Date: 2004-4-2
 * Time: 17:02:49
 */

package org.cnmarc4j;

import java.io.UnsupportedEncodingException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.cnmarc4j.cnmarc.MarcConstants;
import org.cnmarc4j.cnmarc.Leader;
import org.cnmarc4j.cnmarc.Tag;
import org.grlea.log.SimpleLogger;

import java.io.ByteArrayInputStream;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.io.IOException;

/**
 * <p>Parses CNMARC records and reports events to the
 * <code>DefaultHandler</code> and optionally the <code>ErrorHandler</code>.
 * </p>
 *
 * @see MarcHandler
 * @see DefaultHandler
 * @see ErrorHandler
 */
public class CNMarcReader implements MarcReader {

    private static final SimpleLogger log = new SimpleLogger(CNMarcReader.class);
    
    /** default marc record character encoding **/
    private String charsetName = "GB18030";

    /** bytes have already read */
    private int bytesRead = 0;

    /** control number of the record */
    private String controlNumber = null;

    /** The record terminator */
    private static final int RT = MarcConstants.RT;

    /** The field terminator */
    private static final int FT = MarcConstants.FT;

    /** The data element identifier */
    private static final int US = MarcConstants.US;

    /** The blank character */
    private static final int BLANK = MarcConstants.BLANK;

    /** The MarcHandler object. */
    private MarcHandler mh;

    /** The ErrorHandler object */
    private ErrorHandler eh;

    public CNMarcReader() {
    }
    
    /**
     * <p>Sends a MARC record to the parser. This will use default character
     * encoding to parse the record. Default character encoding is GB18030.</p>
     *
     * @param input the MARC record
     */
    public void parse(String input) throws IOException {
        parse(new ByteArrayInputStream(input.getBytes(charsetName)));
    }

    /**
     * <p>Sends a MARC record to the parser.</p>
     *
     * @param input the MARC record
     * @param charsetName MARC record character encoding
     */
    public void parse(String input, String charsetName) throws IOException {
        this.charsetName = charsetName;
        parse(new ByteArrayInputStream(input.getBytes(charsetName)));
    }

    private void parse(ByteArrayInputStream input) throws IOException {
        parse(new BufferedInputStream(input));
    }

    private void parse(InputStream input) throws IOException {
        log.entry("enter parse()");
        
        if (mh != null)
            mh.startCollection();

        byte[] ldr = new byte[24];
        bytesRead += input.read(ldr);
        log.debug("---------------------------------------");
        log.debugObject("Leader", new String(ldr));
        Leader leader = new Leader(new String(ldr));

        int baseAddress = leader.getBaseAddressOfData();
        log.debugObject("baseAddress", baseAddress);

        // 1 是字段分隔符FT(001E)
        int dirLength = baseAddress - (24 + 1);

        byte[] bdir = new byte[dirLength];
        log.debugObject("dirLength", bdir.length);
        log.debugObject("bytesRead", bytesRead);
        log.debugObject("available", input.available());

        // read()方法的第2个参数是指当前读入的第1个字节存贮在byte[]数组中的位置
        // 因此，input的位置指针是由length决定，请仔细阅读文档
        bytesRead += input.read(bdir);

        log.debugObject("bdir", new String(bdir));
        log.debugObject("bytesRead", bytesRead);

        // 使用平台默认编码把byte[] bdir构造为String dir处理
        // 因为dir永远都是ascii字符，所以这样处理不会有问题
        // 或是应该像marc4j那样处理
        String dir = new String(bdir);

        // 当前record中著录的款目数目
        int entries = dirLength / 12;

        // String[]数组存储所著录的每个字段的名称
        String[] tag = new String[entries];

        // int[]数组存储所著录的每个款目的实际长度
        int[] length = new int[entries];

        // int[]数组存储所著录的每个款目在整个record中的开始位置
        int[] position = new int[entries];

        // 循环读取dir中每个款目的信息，index是读取整个dir的位置标志
        for (int i = 0, index = 0; i < entries; i++, index += 12) {
            tag[i]
                = dir.substring(index, index + 3);
            length[i]
                = Integer.parseInt(dir.substring(index + 3, index + 7));
            position[i]
                = Integer.parseInt(dir.substring(index + 7, index + 12));
            /*System.out.println(tag[i] + " "
                    + length[i] + " "
                    + position[i] + "\n");*/
        }

        // 读取完dir中所有款目的信息后，读入的下一个字符应该是FT(001E)字段分隔符
        // 如果不是的话，读取有误，报告错误
        if (input.read() != FT) {
            log.debug("dir的最后一个字符不是字段分隔符FT");
            //System.exit(1);  // should do sth more
        }

        bytesRead++;  // 整个marc的读取计数器加1
        log.debugObject("bytesRead", bytesRead);

        for (int i = 0; i < entries; i++) {
            byte[] field = new byte[length[i]];
            bytesRead += input.read(field);

            // 字段最后一个字符是字段分隔符
            if (field[field.length - 1] != FT) {
                log.debug("当前读取字段的最后一个字符不是字段分隔符FT");
                //System.exit(1);  // should do sth more
            }

            // 如果是控制字段
            // Tag前两位是00的就是控制字段。只有001和005
            if (Tag.isControlField(tag[i])) {
                parseControlField(tag[i], field);
            } else {  // 否则是数据字段
                parseDataField(tag[i], field);
            }
        }

        if (input.read() != RT) {
            log.debug("当前记录的最后一个字符不是记录结束符RT");
            //System.exit(1);  // should do sth more
        }

        bytesRead++;

        if (mh != null)
            mh.endRecord();

        input.close();

        if (mh != null) {
            mh.endCollection();
        }

        log.debugObject("bytesRead", bytesRead);
        log.debug("---------------------------------------");

        // 计数器置零
        bytesRead = 0;
        log.exit("exit parse()");
    }

    private void parseControlField(String tag, byte[] bfield) {
        if (bfield.length < 2 && eh != null) {
            // should do sth more
        }

        if (Tag.isControlNumberField(tag))  // 如果是001字段(记录控制号)
            setControlNumber(trimFT(bfield));  // 设置控制号

        if (mh != null)
            mh.controlField(tag, trimFT(bfield));  // 通知MarcHandler遇到控制字段
    }

    private void parseDataField(String tag, byte[] bfield) {
        // indicators defaulting to a blank value
        char ind1 = BLANK;  // 指示符1
        char ind2 = BLANK;  // 指示符2
        char code = BLANK;  // 子字段代码，比如a，b

        StringBuffer data = null;

        try {
            // 使用文件编码将字节数组转为字符数组
            // 此时1个汉字与1个英文字母一样被看作 1 个字符
            char[] field = new String(bfield, charsetName).toCharArray();            

            if (field.length < 4 && eh != null) {
            // should do sth more
            } else {
                ind1 = field[0];  // 取得指示符1
                ind2 = field[1];  // 取得指示符2
            }
            if (mh != null) {
                mh.startDataField(tag, ind1, ind2);
            }  // 通知 MarcHandler 开始数据字段

            // 紧跟在2个指示符后面的应该是子字段分隔符US(001F)
            if (field[2] != US && field.length > 3 && eh != null) {
            // should do sth more
            }

            // 处理当前数据字段内的所有内容（比如200字段）
            // 取完两个指示符后，指针从 2 开始
            for (int i = 2; i < field.length; i++) {
                char c = field[i];  // 取得当前字符
                switch (c) {
                    // 如果是子字段分隔符
                    case US:
                        if (data != null) {
                            // 通知 MarcHandler 前面已经读入的子字段的代码及数据
                            reportSubfield(code, data);
                            data.delete(0, data.length());
                        } else {
                            data = new StringBuffer();
                        }
                        code = field[i + 1];  // 取得子字段代码，比如a，b
                        i++;
                        break;

                    // 如果是字段结束符
                    case FT:
                        if (data != null) // 通知 MarcHandler 前面已经读入的子字段的代码及数据
                        {
                            reportSubfield(code, data);
                        }
                        break;

                    // 默认将具体数据加入data
                    default:
                        if (data != null) {
                            data.append(c);
                        }
                }
            }
            if (mh != null) {
                mh.endDataField(tag);
            }  // 处理完毕后，通知MarcHandler当前数据字段结束

        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println(e);
            System.exit(1);     
        } catch (UnsupportedEncodingException ex) {
            System.out.println(ex);
            System.exit(1);
        }
    }

    private void reportSubfield(char code, StringBuffer data) {
        if (mh != null)
            mh.subfield(code, new String(data).toCharArray());
    }

    private char[] trimFT(byte[] bfield) {
        StringBuffer sb = new StringBuffer();
        try {            
            // 使用文件编码将字节数组转为字符数组
            // 此时1个汉字与1个英文字母一样被看作 1 个字符
            char[] field = new String(bfield, charsetName).toCharArray();
            for (int i = 0; i < field.length; i++) {
                // 001字段中不可能有中文，所以可以这样处理
                char c = field[i];
                switch (c) {
                    case FT:
                        break;
                    default:
                        sb.append(c);
                }
            }            
        } catch (UnsupportedEncodingException ex) {
            Logger.getLogger(CNMarcReader.class.getName()).log(Level.SEVERE, null, ex);
        }
        return sb.toString().toCharArray();
    }

    private void setControlNumber(String controlNumber) {
        this.controlNumber = controlNumber;
    }

    private void setControlNumber(char[] controlNumber) {
        this.controlNumber = new String(controlNumber);
    }

    /**
     * <p>Registers the <code>MarcHandler</code> implementation.</p>
     *
     * @param mh the {@link MarcHandler} implementation
     */
    public void setMarcHandler(MarcHandler mh) {
        this.mh = mh;
    }

}
